﻿using System;
using System.ComponentModel;
using System.Linq;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Threading;
using ZOIL.Framework.Core;

namespace ZOIL.Framework.Presentation
{
    /// <summary>
    /// this class is responsible for smoothly animating between to points in the ZInformationLandscape
    /// </summary>
    public class ZLandscapeAnimation
    {
        #region Private Fields

#if DEBUG
        private int _lineNum = 1000;
#endif

        private const int _performBoostOnGreaterThan = 5;

        private readonly object _renderLock = new object();
        private int _frameCountInThisAnimation = 0;
        private int _millisOfLastOnRender;

        //default zoom rate in factors per second
        private const double DefaultZoomRate = 35;
        //minimum value in ms which is added on zoom to ensure minimum time
        private const double MinimumZoomTime = 1000;
        private const double Precision = 1E-10;

        private double _zoomTimeForCurrentAnimation;

        //needed in OnRender
        private TranslateTransform _savedTranslateTransform;
        private ScaleTransform _savedScaleTransform;
        private int _firstTimestamp;

        // Transformations
        private TransformGroup _transformGroup = new TransformGroup();
        private TranslateTransform _translateTransform = new TranslateTransform(0.0, 0.0);
        private ScaleTransform _scaleTransform = new ScaleTransform(1.0, 1.0, 0.0, 0.0);

        private ZInformationLandscape _informationLandscape;

        // Status
        private bool _active;
        private ZLandscapePosition _targetPosition;

        //Cache ZLandscapeAnimationEventArgs here
        private readonly ZLandscapeAnimationEventArgs _args;

        private readonly DispatcherTimer _bitmapCacheTimer;

        #endregion

        #region events

        // Events
        /// <summary>
        /// 
        /// </summary>
        public event EventHandler<ZLandscapeAnimationEventArgs> Finished;

        /// <summary>
        /// 
        /// </summary>
        public event EventHandler<ZLandscapeAnimationEventArgs> Changed;

        #endregion

        #region Public Fields / Properties

        /// <summary>
        /// 
        /// </summary>
        public bool IsActive
        {
            get { return _active; }
        }

        private FrameworkElement _target;

        public FrameworkElement Target
        {
            get { return _target; }
            set { _target = value; }
        }

        /// <summary>
        /// 
        /// </summary>
        public Transform RenderTransform
        {
            get { return _transformGroup; }
            set
            {
                // TODO: check transformation type
                _transformGroup = (TransformGroup)value;
                foreach (var t in _transformGroup.Children)
                {
                    if (t is ScaleTransform)
                        _scaleTransform = (ScaleTransform)t;
                    else if (t is TranslateTransform)
                        _translateTransform = (TranslateTransform)t;
                }
            }
        }

        /// <summary>
        /// 
        /// </summary>
        public ZLandscapePosition Position
        {
            get
            {
                return new ZLandscapePosition(
                    _scaleTransform.ScaleX, new Point(
                        _translateTransform.X, _translateTransform.Y));
            }
        }

        public double MaxZoomFactor
        {
            get;
            set;
        }

        #endregion

        #region Constructor

        /// <summary>
        /// Initiates a new animation.
        /// </summary>
        /// <param name="landscape">The information landscape using this landscape animation.</param>
        /// <param name="target">The component that will provide a container
        /// for the translation and scale transformations applied by the
        /// animation.</param>
        public ZLandscapeAnimation(ZInformationLandscape landscape, FrameworkElement target)
        {
            _informationLandscape = landscape;
            Target = target;

            _bitmapCacheTimer = new DispatcherTimer(DispatcherPriority.Render)
                                    {
                                        Interval = TimeSpan.FromMilliseconds(_informationLandscape.CacheModeInterval)
                                    };

            // Attach property change notifaction to bitmap cache timer
            var cacheModeIntervalProperty = DependencyPropertyDescriptor.FromProperty(ZInformationLandscape.CacheModeIntervalProperty, typeof(ZInformationLandscape));
            cacheModeIntervalProperty.AddValueChanged(_informationLandscape, (sender, args) => _bitmapCacheTimer.Interval = TimeSpan.FromMilliseconds(_informationLandscape.CacheModeInterval));

            _bitmapCacheTimer.Tick += (sender, e) =>
            {
                DetachBitmapCache();
                StopBitmapCacheTimer();
            };

            // Setup transformation group
            _transformGroup.Children.Add(_translateTransform);
            _transformGroup.Children.Add(_scaleTransform);
            _target.RenderTransform = _transformGroup;

            // Set current position in our information landscape
            // TODO: Take current transformations, if present (in source).
            //...
            _targetPosition = Position;

            _args = new ZLandscapeAnimationEventArgs(this);
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Called for each frame from WPF before content is rendered and after content has been layouted
        /// Translation and Scaling are performed gradually here in order to get a "smooth" animation
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void OnRender(object sender, EventArgs e)
        {
            lock (_renderLock)
            {
                // Update timestamp
                var tickCount = Environment.TickCount;
                var timediffToLast = tickCount - _millisOfLastOnRender;
                var timediff = tickCount - _firstTimestamp;
                _millisOfLastOnRender = tickCount;

                // check if OnRender is called multiple times for same frame (this happens sometimes 3 or more times after another!)
                if (timediffToLast == 0) return;

                _frameCountInThisAnimation++;

                //check if the target of the animation is reached
                if (timediff >= _zoomTimeForCurrentAnimation)
                {
                    _translateTransform.X = _targetPosition.Pan.X;
                    _translateTransform.Y = _targetPosition.Pan.Y;
                    _scaleTransform.ScaleX = _targetPosition.Zoom;
                    _scaleTransform.ScaleY = _targetPosition.Zoom;

#if DEBUG
                    ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, ("(" + _lineNum++ + ")" + "Finished zoom-animation"));
#endif
                    CompositionTargetExtension.FrameUpdating -= OnRender;

                    _active = false;

                    // Raise update event
                    if (Changed != null)
                        Changed(this, _args);

                    // Raise finish event
                    if (Finished != null)
                        Finished(this, _args);

                    return;
                }


                //space scale formula: ((z2 - z1) / (z2x2 - z1x1)) (see Furnas + Bederson paper about Space Scale Diagrams)

                //the scale factor of the source and target points
                var z1 = _savedScaleTransform.ScaleX;
                var z2 = _targetPosition.Zoom;

                //the x coordinate of the source and target points
                var x1 = _savedTranslateTransform.X;
                var x2 = _targetPosition.Pan.X;

                //the y coordinate of the source and target points
                var y1 = _savedTranslateTransform.Y;
                var y2 = _targetPosition.Pan.Y;
                double mx = 0, my = 0;    // random value only for intialization

                // if start(x1,y1) != end(x2,y2) interpolate in space and calculate scale
                if (!(x1 == x2 && y1 == y2))
                {
                    // calculate mx and/or my only if there is a pan in the corresponding direction
                    // (mx and my are constant factors that are needed for Furnas & Bederson space scale formula)
                    if (x1 != x2) mx = ((z2 - z1) / (x2 * z2 - x1 * z1));
                    if (y1 != y2) my = ((z2 - z1) / (y2 * z2 - y1 * z1));

                    // calculate the pan transformation
                    var transformX = GetPanPointAtTime(x1, x2, timediff, _zoomTimeForCurrentAnimation);
                    var transformY = GetPanPointAtTime(y1, y2, timediff, _zoomTimeForCurrentAnimation);

                    // calculate the current zoom factor as a function of the previously calculated pan values
                    var currentZoomFactorX = (z1 - mx * z1 * x1) / (1.0 - mx * transformX);    // random value only for intialization
                    var currentZoomFactorY = (z1 - my * z1 * y1) / (1.0 - my * transformY);    // random value only for intialization

                    // replace calculated zoom factor values if there are troubles with very small differences
                    if (Double.IsNaN(currentZoomFactorX) && !Double.IsNaN(currentZoomFactorY)) currentZoomFactorX = currentZoomFactorY;
                    if (Double.IsNaN(currentZoomFactorY) && !Double.IsNaN(currentZoomFactorX)) currentZoomFactorY = currentZoomFactorX;
                    if (Double.IsNaN(currentZoomFactorX) && Double.IsNaN(currentZoomFactorY))
                    {
                        currentZoomFactorX = z2;
                        currentZoomFactorY = z2;
                    }

                    // apply the pan transformation first
                    _translateTransform.X = transformX;
                    _translateTransform.Y = transformY;

                    //// Restrict zoom factor to maximum zoom factor if set on ZInformationLandscape.
                    //currentZoomFactorX = currentZoomFactorX > MaxZoomFactor ? MaxZoomFactor : currentZoomFactorX;
                    //currentZoomFactorY = currentZoomFactorY > MaxZoomFactor ? MaxZoomFactor : currentZoomFactorY;

                    // apply the zoom factors if there is a pan in the corresponding direction, otherwise use the other one
                    if (x1 != x2) _scaleTransform.ScaleX = currentZoomFactorX;
                    else _scaleTransform.ScaleX = currentZoomFactorY;

                    if (y1 != y2) _scaleTransform.ScaleY = currentZoomFactorY;
                    else _scaleTransform.ScaleY = currentZoomFactorX;
                }
                // if start(x1,y1) == end(x2,y2) only interpolate scale
                else
                {
                    if (z1 == z2)
                    {
                        // if x1,y1,z1 == x2,y2,z2 just return
                        return;
                    }

                    _scaleTransform.ScaleX = GetPanPointAtTime(z1, z2, timediff, _zoomTimeForCurrentAnimation);
                    _scaleTransform.ScaleY = _scaleTransform.ScaleX;
                }

                // Raise update event
                if (Changed != null)
                    Changed(this, _args);
            }
        }

        /// <summary>
        /// calculates the pan point for a given moment in time
        /// </summary>
        /// <param name="startx">value at the start point</param>
        /// <param name="endx">value at the end point</param>
        /// <param name="elapsedTime">time elapsed since the animation started (in miliseconds)</param>
        /// <param name="totalAnimationTime">time the animation needs to be completed (in miliseconds)</param>
        /// <returns></returns>
        private static double GetPanPointAtTime(double startx, double endx, double elapsedTime, double totalAnimationTime)
        {
            var time = elapsedTime / totalAnimationTime;

            // elliptic interpolation
            return Math.Sqrt(1.0 - (time - 1.0) * (time - 1.0)) * (endx - startx) + startx;

            // polynomic interpolation
            //return (-1.1698 * Math.Pow(time, 3) + 1.4151 * Math.Pow(time, 2) + 0.7547 * time) * (endx - startx) + startx;

            // linear interpolation
            // return startx + (endx - startx) * time;

            // cosinus interpolation
            //return startx + (0.5 * (1.0 - Math.Cos(Math.PI * time)) * (endx - startx));
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Starts the zoom animation
        /// </summary>
        public void Start()
        {
            if (!IsActive)
            {
                _active = true;
                _firstTimestamp = Environment.TickCount;
                _millisOfLastOnRender = _firstTimestamp;
                _frameCountInThisAnimation = 0;

                _savedScaleTransform = _scaleTransform.Clone();
                _savedTranslateTransform = _translateTransform.Clone();

                var zoomingIn = _savedScaleTransform.ScaleX < _targetPosition.Zoom;
                var wantedScaleFactor = zoomingIn ? (_targetPosition.Zoom / _savedScaleTransform.ScaleX) : (_savedScaleTransform.ScaleX / _targetPosition.Zoom);
                _zoomTimeForCurrentAnimation = wantedScaleFactor / (DefaultZoomRate / 1000.0) + MinimumZoomTime;

                CompositionTargetExtension.FrameUpdating += OnRender;

#if DEBUG
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" + "Started zoom-animation (duration " + _zoomTimeForCurrentAnimation + ") Hash:(" + this.GetHashCode() + ").");
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" +
                    "Start of animation: (" + _savedTranslateTransform.X + " ; " + _savedTranslateTransform.Y + ") * " + _savedScaleTransform.ScaleX + "; " + _savedScaleTransform.ScaleY);
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" +
                    "Dest. of animation: (" + _targetPosition.Pan.X + " ; " + _targetPosition.Pan.Y + ") * " + _targetPosition.Zoom);
#endif
            }
        }

        /// <summary>
        /// Pauses the current animtion
        /// </summary>
        public void Pause()
        {
            CompositionTargetExtension.FrameUpdating -= OnRender;
            _active = false;

#if DEBUG
            ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" + "Paused zoom-animation.");
#endif
        }

        /// <summary>
        /// stops the current animation and sets the target position to the current position
        /// </summary>
        public void Stop()
        {
            CompositionTargetExtension.FrameUpdating -= OnRender;
            _active = false;
            _targetPosition = Position;

            // Raise finish event
            if (Finished != null)
                Finished(this, _args);

#if DEBUG
            ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" + "Stopped zoom-animation.");
#endif
        }

        /// <summary>
        /// moves the landscape to the given target point (by starting animation or setting directly)
        /// </summary>
        /// <param name="position">the target point to move to</param>
        /// <param name="animated">animated moving</param>
        public void Goto(ZLandscapePosition position, bool animated)
        {
            bool ignored = false;
            // ignore requests for animation, if start and destination are roughly the same
            if (Math.Abs(_scaleTransform.ScaleX - position.Zoom) <= Precision
                 && Math.Abs(_scaleTransform.ScaleY - position.Zoom) <= Precision
                 && Math.Abs(_translateTransform.X - position.Pan.X) <= Precision
                 && Math.Abs(_translateTransform.Y - position.Pan.Y) <= Precision
                )
            {
                animated = false;
                ignored = true;
#if DEBUG
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" + "IGNORED zoom-animation -> setting pos to destination unanimated.");
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" +
                    "Start of animation: (" + _translateTransform.X + " ; " + _translateTransform.Y + ") * " + _scaleTransform.ScaleX + "; " + _scaleTransform.ScaleY);
                ZDebug.Log(DebugLevel.Info, DebugCategories.Animations, "(" + _lineNum++ + ")" +
                    "Dest. of animation: (" + position.Pan.X + " ; " + position.Pan.Y + ") * " + position.Zoom);
#endif
            }

            if (double.IsNaN(position.Pan.X) || double.IsNaN(position.Pan.Y))
                return;

            AttachBitmapCache();

            // Restrict zoom factor to maximum zoom factor if set on ZInformationLandscape.
            var zoom = position.Zoom;
            zoom = zoom > MaxZoomFactor ? MaxZoomFactor : zoom;

            if (animated)
            {
                Finished += AnimationFinished;

                Pause();

                // Apply zoom
                position.Zoom = zoom;

                _targetPosition = position;
                Start();
            }
            else
            {
                StopBitmapCacheTimer();

                if (IsActive)
                    Stop();

                _scaleTransform.ScaleX = zoom;
                _scaleTransform.ScaleY = zoom;

                if (Math.Abs(zoom - position.Zoom) < 0.00001)
                {
                    _translateTransform.X = position.Pan.X;
                    _translateTransform.Y = position.Pan.Y;
                }

                // Raise update event
                if (Changed != null)
                    Changed(this, _args);
                if (ignored && Finished != null)
                    Finished(this, _args);

                StartBitmapCacheTimer();
            }
        }

        void AnimationFinished(object sender, ZLandscapeAnimationEventArgs e)
        {
            DetachBitmapCache();
            Finished -= AnimationFinished;
        }

        void AttachBitmapCache()
        {
            if (!_informationLandscape.CacheModeEnabled) return;

            var count = 0;

            var children = FindVisibleChildren0(Target).ToList();

            foreach (var child in children)
                if (child.CacheMode == null)
                {
                    child.SwapCacheMode();
                    count++;
                }

#if DEBUG
            ZDebug.QuickLog(string.Format("Bitmap cache attached to {0} elements.", count));
#endif
        }

        void DetachBitmapCache()
        {
            var count = 0;

            var children = FindVisibleChildren0(Target).ToList();

            foreach (var child in children)
                if (child.CacheMode != null)
                {
                    child.SwapCacheMode();

                    // child1 = child needs to be there
                    //var child1 = child;
                    //_informationLandscape.Dispatcher.BeginInvoke((Action)(child1.SwapCacheMode), DispatcherPriority.Background);
                    count++;
                }

#if DEBUG
            ZDebug.QuickLog(string.Format("Bitmap cache detached from {0} elements.", count));
#endif

            _informationLandscape.ElementsNotInCacheMode = children.Count;
        }

        void StartBitmapCacheTimer()
        {
            if (!_informationLandscape.CacheModeEnabled) return;

            _bitmapCacheTimer.Start();
        }

        void StopBitmapCacheTimer()
        {
            if (!_informationLandscape.CacheModeEnabled) return;

            _bitmapCacheTimer.Stop();
        }

        #endregion

        private static List<FrameworkElement> _componentIndex;

        private void BuildComponentIndex()
        {
            var components = FindVisibleChildren1<ZComponent>(Target);

            _componentIndex = new List<FrameworkElement>(components);
        }

        public IEnumerable<ZComponent> FindVisibleChildren0(Visual visual)
        {
            if (visual == null)
                yield break;

            var geometry = new RectangleGeometry(_informationLandscape.Viewport0);

            var components = ZHitTestHelper.GetElementsInGeometry<ZComponent>(geometry, visual);

            foreach (var component in components)
                yield return component;
        }

        private static IEnumerable<T> FindVisibleChildren1<T>(DependencyObject dependencyObject) where T : FrameworkElement
        {
            if (_componentIndex != null)
            {
                foreach (var component in _componentIndex)
                {
                    if (!(component as ZComponent).IsOffScreen)
                        yield return (T)component;
                }
                yield break;
            }

            if (dependencyObject == null) yield break;

            for (var i = 0; i < VisualTreeHelper.GetChildrenCount(dependencyObject); i++)
            {
                var child = VisualTreeHelper.GetChild(dependencyObject, i);
                //if (child is T && !(child as ZComponent).IsOffScreen)
                if (child is T)
                    yield return (T)child;

                if (!(child is ZComponent))
                    foreach (var childOfChild in FindVisibleChildren1<T>(child))
                        yield return childOfChild;
            }
        }
    }
}